package edu.northwestern.cbits.purple_robot_manager.models.trees; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Map; public class BranchNode extends TreeNode { /** * Thrown when a tree is evaluated and a value is not available to match any * of the associated conditions. */ public static class MissingValueException extends TreeNodeException { public MissingValueException(String message) { super(message); } private static final long serialVersionUID = 7858585916379498941L; } private ArrayList<Condition> _conditions = new ArrayList<>(); /** * Different kinds of tests and comparisons. (Not all are currently * implemented. */ public enum Operation { LESS_THAN, LESS_THAN_OR_EQUAL_TO, MORE_THAN, MORE_THAN_OR_EQUAL_TO, EQUALS, EQUALS_CASE_INSENSITIVE, CONTAINS, CONTAINED_BY, STARTS_WITH, ENDS_WITH, DEFAULT // Always // passes. // Typically // used // as // a // catch-all // for // missing // data // scenarios. } /** * Represents a test run against the feature data representing the real * world. Conditions consist of a test (feature to test, test operation, and * value/threshold) and a "next node" that is returned when the state of the * world passes the condition. */ public static class Condition { public static final int DEFAULT_PRIORITY = 0; public static final int LOWEST_PRIORITY = Integer.MIN_VALUE; public static final int HIGHEST_PRIORITY = Integer.MAX_VALUE; String _feature = null; Object _value = null; BranchNode.Operation _operation = Operation.DEFAULT; TreeNode _node = null; int _priority = Condition.DEFAULT_PRIORITY; /** * Builds a condition. * * @param op * Test operation. * @param feature * Key of the feature to test. * @param value * Test value or threshold. * @param priority * Priority of the condition. Conditions with higher priority * are evaluated before those with lower priorities. * @param node * TreeNode associated with fulfilling this condition. */ public Condition(Operation op, String feature, Object value, int priority, TreeNode node) { this._operation = op; this._feature = feature; this._value = value; this._priority = priority; this._node = node; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ public String toString() { return this._feature + " " + this._operation.name() + " " + this._value + " (" + this._priority + ")"; } /** * Evaluates the provided features against this condition. * * @param features * Key-value pairs representing the state of the world. * @return True if the features fulfills this condition, false * otherwise. * * @throws TreeNodeException * Thrown on error evaluating the coditions. */ public boolean evaluate(Map<String, Object> features) throws TreeNodeException { Object value = features.get(this._feature); if (value == null && this._operation != Operation.DEFAULT) { // We're missing a value to test and this isn't a DEFAULT node. return false; } switch (this._operation) { case LESS_THAN: return Condition.testLessThan(this._value, value); case LESS_THAN_OR_EQUAL_TO: return Condition.testLessThanOrEqualTo(this._value, value); case MORE_THAN: return Condition.testMoreThan(this._value, value); case MORE_THAN_OR_EQUAL_TO: return Condition.testMoreThanOrEqualTo(this._value, value); case EQUALS: return Condition.testEquals(this._value, value); case EQUALS_CASE_INSENSITIVE: return Condition.testEqualsCaseInsensitive(this._value, value); case CONTAINS: return Condition.testContains(this._value, value); case CONTAINED_BY: return Condition.testEqualsContainedBy(this._value, value); case STARTS_WITH: return Condition.testStartsWith(this._value, value); case ENDS_WITH: return Condition.testEndsWith(this._value, value); case DEFAULT: return true; } return false; } /** * Tests if value >= test. * * @param test * @param value * @return * @throws TreeNodeException */ @SuppressWarnings( { "rawtypes", "unchecked" }) private static boolean testMoreThanOrEqualTo(Object test, Object value) throws TreeNodeException { if (test instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Test does not implement Comparable."); if (value instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Value does not implement Comparable."); Comparable testComparable = (Comparable) test; Comparable valueComparable = (Comparable) value; int result = testComparable.compareTo(valueComparable); return result < 1; } /** * Tests if value <= test. * * @param test * @param value * @return * @throws TreeNodeException */ @SuppressWarnings( { "rawtypes", "unchecked" }) private static boolean testLessThanOrEqualTo(Object test, Object value) throws TreeNodeException { if (test instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Test does not implement Comparable: " + test); if (value instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Value does not implement Comparable: " + value); Comparable testComparable = (Comparable) test; Comparable valueComparable = (Comparable) value; int result = testComparable.compareTo(valueComparable); return result > -1; } /** * Tests if value > test. * * @param test * @param value * @return * @throws TreeNodeException */ @SuppressWarnings( { "rawtypes", "unchecked" }) private static boolean testMoreThan(Object test, Object value) throws TreeNodeException { if (test instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Test does not implement Comparable."); if (value instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Value does not implement Comparable."); Comparable testComparable = (Comparable) test; Comparable valueComparable = (Comparable) value; int result = testComparable.compareTo(valueComparable); return result < 0; } /** * Tests if value < test. * * @param test * @param value * @return * @throws TreeNodeException */ @SuppressWarnings( { "rawtypes", "unchecked" }) private static boolean testLessThan(Object test, Object value) throws TreeNodeException { if (test instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Test does not implement Comparable."); if (value instanceof Comparable<?> == false) throw new TreeNode.TreeNodeException("Value does not implement Comparable."); Comparable testComparable = (Comparable) test; Comparable valueComparable = (Comparable) value; int result = testComparable.compareTo(valueComparable); return result > 0; } /** * Tests if value.equals(test). * * @param test * @param value * @return * @throws TreeNodeException */ private static boolean testEquals(Object test, Object value) throws TreeNodeException { return test.equals(value); } private static boolean testEndsWith(Object test, Object value) throws TreeNodeException { throw new TreeNode.TreeNodeException("Unimplemented comparison."); } private static boolean testEqualsCaseInsensitive(Object test, Object value) throws TreeNodeException { throw new TreeNode.TreeNodeException("Unimplemented comparison."); } private static boolean testStartsWith(Object test, Object value) throws TreeNodeException { throw new TreeNode.TreeNodeException("Unimplemented comparison."); } private static boolean testContains(Object test, Object value) throws TreeNodeException { throw new TreeNode.TreeNodeException("Unimplemented comparison."); } private static boolean testEqualsContainedBy(Object test, Object value) throws TreeNodeException { throw new TreeNode.TreeNodeException("Unimplemented comparison."); } /** * Returns the node associated with this condition. * * @return Associated TreeNode * @throws TreeNode.TreeNodeException * Thrown if no node is associated with this condition. */ public TreeNode getNode() throws TreeNode.TreeNodeException { if (this._node == null) throw new TreeNode.TreeNodeException("Null tree node encountered."); return this._node; } } public BranchNode(String name) { super(name); } public BranchNode() { super(null); } /** * Adds a test and subtree to this node. * * @param op * Test operation * @param feature * Feature to test * @param value * Test or threshold. * @param priority * Priority of this test. * @param node * Associated TreeNode on passing of test. */ public void addCondition(Operation op, String feature, Object value, int priority, TreeNode node) { this._conditions.add(new Condition(op, feature, value, priority, node)); Collections.sort(this._conditions, new Comparator<Condition>() { public int compare(Condition one, Condition two) { if (one._priority > two._priority) return -1; else if (one._priority < two._priority) return 1; return one._operation.compareTo(two._operation); } }); } /** * Returns the prediction generated bt this node's descendants that map to * the values in features. * * @see edu.northwestern.cbits.purple_robot_manager.models.trees.TreeNode#fetchPrediction(java.util.Map) */ public Map<String, Object> fetchPrediction(Map<String, Object> features) throws TreeNode.TreeNodeException { for (Condition condition : this._conditions) { if (condition.evaluate(features)) { // Test passed - recurse down the test's associated node and // continue... return condition.getNode().fetchPrediction(features); } } throw new TreeNode.TreeNodeException( "No matching condition for this set of features. Add a DEFAULT condition perhaps?"); } /* * (non-Javadoc) * * @see * edu.northwestern.cbits.purple_robot_manager.models.trees.TreeNode#toString * (int) */ public String toString(int indent) throws TreeNodeException { StringBuilder sb = new StringBuilder(); String newline = System.getProperty("line.separator"); for (Condition condition : this._conditions) { if (sb.length() > 0) sb.append(newline); for (int i = 0; i < indent; i++) sb.append(" "); sb.append(condition.toString()); sb.append(newline); sb.append(condition.getNode().toString(indent + 1)); } return sb.toString(); } public void addDefaultCondition(TreeNode node) { this._conditions.add(new Condition(Operation.DEFAULT, "foo", "bar", Condition.LOWEST_PRIORITY, node)); Collections.sort(this._conditions, new Comparator<Condition>() { public int compare(Condition one, Condition two) { if (one._priority > two._priority) return -1; else if (one._priority < two._priority) return 1; return one._operation.compareTo(two._operation); } }); } }